using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO; //Filesystem access
using System.ServiceProcess;
using System.Text;
using System.Text.RegularExpressions;
using System.Timers; //I has a timer
using Microsoft.Win32; //Registry access

//This program utilizes IndySockets - if you don't have it installed, Plox won't compile. This code was
//developed against IndySockets (VCL) v10.1.5 - if you use IndySockets (CLR), you may have to do some
//debugging and renaming. IndySockets is available courtesy of the IndySockets Pit Crew and AtoZed
//Software at: http://www.atozed.com/indy/plus/Files.EN.aspx

//IndySockets is open source under a modified BSD license and is compatable with MS-RL.

using Indy.Sockets; //FTP

//I'm going to say it right here: since the purpose of Plox is to delete stuff, it could be abused to create
//all kinds of general havoc. But, since there is no way for me to determine this at runtime, there is also
//no way for me to prevent it. Therefore, to prevent possible annoyance to legitimate users, and since it
//would be trivial to circumvent, Plox doesn't "filter out" any paths / files / etc. However, elevated
//privileges are required to install a service in the first place, so if a malicious user can install Plox
//in the first place, the battle is already lost.

//Changes to Version 1.1:
//
//Fixed a potential bug - Plox now is case-insensitive when it looks for it's values in the registry.
//
//When checking for the value timer, an error handler is no longer invoked if the value is negative.
//Saves a handful of cycles, more graceful code.
//
//Added the ability to specify expiration by time from created and time from last modified.
//I don't have an immediate use for this, but it was an easy change and it has an air of usefulness about it.

//Changes to Version 1.2
//
//Fixed two bugs - writes to the Windows event log were not enclosed in try blocks and could have caused an
//unhandled exception, and mask was not validated, which caused a potential directory traversal bug.
//
//Plox now supports recursion, and can be told whether to delete empty subdirectories. This turned out to
//be *almost* a complete rewrite of PloxSvc.cs, but I am much happier with the code now. Support for
//user-definable logging and halting was also added, but will not be exposed in PloxEd until version 2.0. 

//Changes to Version 2.0
//
//Added FTP support. Also added support for finite recursion depth, which could prevent infinite loopage on
//poorly-constructed remote filesystems. I'd like to add SMB/CIFS support, but I don't want to write my own
//implementation and there don't seem to be any libraries readily available. The Framework handles SMB the
//same way that Windows does, which is to say, poorly, so I can't use it. Specifically, there's no real way
//to authenticate as a particular user without having to call "NET SEND" from inside my code - there's probably
//not a more bletcherous way to do things than that.

//Planned for Version 2.1
//
//I plan to thoroughly multithread Plox for this release. No feature adds are planned, this is a performance
//tweak release only. This is the last planned release, apart from bugfixes.

//Plox is licensed under the Microsoft Reciprocal License (MS-RL), and was copied right in the 
//years 2007-2008 G.U.E. by KingFlathead.

namespace Plox
{
    class PloxSvc : ServiceBase
    {
        //Because List has List.Contains, that's why. Also, if you change this, handlers for the
        //new scheme must be added to JobExec, Explode, Delete, and Rmdir.
        readonly List<string> supportedSchemes = new List<string>(new string[]{"file", "ftp"});

        List<string> ploxDescs = new List<string>(); //Job descriptions, only used for logging
        List<Regex> ploxRegexes = new List<Regex>();
        List<Uri> ploxUris = new List<Uri>();
        List<ushort> ploxExpNew = new List<ushort>(); //How old files need to be in days until they expire
        List<ushort> ploxExpMod = new List<ushort>(); //How long files need to be unchanged in days until they expire
        List<byte> ploxRecurseDepth = new List<byte>(); //Is this job recursive?
        List<bool> ploxDelEmpty = new List<bool>(); //If so, do we remove empty subdirectories?

        Timer ploxTimer = new Timer(1800000); //30 minutes

        bool debugging = false; //Used to prevent threadflooding.

        //User configurable - overwritten by GetParams when called. These are the default values.
        bool haltOnFileDelFail = false;
        bool haltOnDirDelFail = false;

        bool logOnFTPError = true; //Set separately since this has the potential to crapflood a log.
        bool redactFTPCredentials = true; //Scrub the username and password from the URI before logging.

        bool logOnLocalDelFail = true;
        bool logOnLocalEnumFail = true;

        bool logToEventLog = true;
        bool logToFlatFile = true;
        string logFile = Environment.SystemDirectory + Path.DirectorySeparatorChar + "ploxlog.txt"; //Working directory for services.

        public PloxSvc() //Defines how we behave as a service
        {
            this.ServiceName = "Plox";
            this.CanStop = true;
            this.CanPauseAndContinue = true;
            this.AutoLog = true;
        }

        public static void Main() //Launches the service thread and that's it
        {
            //Debugging only. Allows us to attach before OnStart executes! Unfortunately, Service Control Manager
            //gives us only 30 seconds before it terminates the thread, since a thread in debugging is effectively
            //hung. Still, better than nothing.
            //System.Diagnostics.Debugger.Break();

            System.ServiceProcess.ServiceBase.Run(new PloxSvc());
        }

        protected override void OnStart(String[] args)
        {
            try //In case the box is locked down super hard and I am not running as LocalSystem
            {
                RegistryKey ploxReg = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Plox");

                GetParams(ploxReg); //Get my parameters - possible in theory to ignore failure, but we won't.

                if (this.debugging)
                    System.Diagnostics.Debugger.Break();

                //Evaluating this if executes GetJobs. Doing it this way eliminates an exception if there are no jobs.
                if (!GetJobs(ploxReg))
                {
                    LogEvent("Plox successfully started, but does not appear to have anything to do and will terminate."
                        , EventLogEntryType.Warning);
                    this.Stop();
                }
            }
            catch (Exception x)
            {
                //OK to use LogEvent - variables that it uses all have default values.
                LogEvent(x.Message, EventLogEntryType.Error);
                this.Stop(); //ploxTimer will never be started, so this kills the thread.
            }

            if (this.debugging)
                this.ploxTimer.AutoReset = false; //Trigger only once.

            this.ploxTimer.Elapsed += new ElapsedEventHandler(PloxDo); //Method to fire.
            this.ploxTimer.Start(); //Go, Speed Racer, go!
        }

        private void GetParams(RegistryKey myReg)
        {
            //If anything causes an exception, it should be caught by OnStart.
            //This should never happen and if it does the resulting error message will not mean anything
            //to a non-developer - however, Plox itself will exit gracefully and not crash.

            uint timerVal;
            bool timerError = false;

            if (System.UInt32.TryParse(myReg.GetValue("timer").ToString(), out timerVal))
            {
                if (timerVal == 0) //For debugging - fire once only.
                {
                    //A short delay - if the timer fires while the service is starting, Plox gets stuck in the
                    //Starting state until all jobs are finished. If this takes more than 30 seconds, Service
                    //Control Manager terminates the thread for hanging on start.
                    this.ploxTimer.Interval = 5000;
                    debugging = true; //Used to configure PloxTimer as a one-shot.
                }
                else
                    this.ploxTimer.Interval = (60000 * timerVal); //mS in one minute
            }
            else //Default value has already been set.
                timerError = true; //Can't log anything yet, don't know where or how to.

            //Everything else is handled on an optional basis - PloxEd should write these values for us,
            //but if a value is not found it is not considered to be an error and defaults will be used.
            //Remember - TryParse fails destructively. Hence, thisVal.

            bool thisVal; //No need to init.

            if (bool.TryParse(myReg.GetValue("HaltOnFileDelFail", "").ToString(), out thisVal))
                this.haltOnFileDelFail = thisVal;

            if (bool.TryParse(myReg.GetValue("HaltOnDirDelFail", "").ToString(), out thisVal))
                this.haltOnDirDelFail = thisVal;

            if (bool.TryParse(myReg.GetValue("LogOnLocalDelFail", "").ToString(), out thisVal))
                this.logOnLocalDelFail = thisVal;

            if (bool.TryParse(myReg.GetValue("LogOnLocalEnumFail", "").ToString(), out thisVal))
                this.logOnLocalEnumFail = thisVal;

            if (bool.TryParse(myReg.GetValue("LogToEventLog", "").ToString(), out thisVal))
                this.logToEventLog = thisVal;

            if (bool.TryParse(myReg.GetValue("LogToFlatFile", "").ToString(), out thisVal))
                this.logToFlatFile = thisVal;

            if (bool.TryParse(myReg.GetValue("LogOnFTPError", "").ToString(), out thisVal))
                this.logOnFTPError = thisVal;

            if (bool.TryParse(myReg.GetValue("RedactFTPCredentials", "").ToString(), out thisVal))
                this.redactFTPCredentials = thisVal;

            this.logFile = myReg.GetValue("logfile", this.logFile).ToString();

            if (this.logToFlatFile)
            {
                try //Attempt to access the log file
                {
                    if (File.Exists(this.logFile))
                        File.OpenWrite(this.logFile).Close();
                    else
                        File.Create(this.logFile).Close();
                }
                catch //Either a value was null or the file was not writable.
                {
                    this.logToFlatFile = false; //Hopefully they didn't disable event log logging as well...
                }
            }

            if(timerError) //Now we can log it.
                LogEvent("Invalid data is stored in timer, using the default value of 30 minutes", EventLogEntryType.Warning);

        } //End of GetParams

        private bool GetJobs(RegistryKey myReg) //The return value is exit status, like TryParse.
        {
            if (myReg.SubKeyCount == 0) //Nothing to do.
                return false;

            RegistryKey[] subKeys = new RegistryKey[myReg.SubKeyCount];

            for (int i = 0; i < subKeys.Length; i++) //Iterate through the subkeys.
                subKeys[i] = myReg.OpenSubKey(myReg.GetSubKeyNames()[i]);

            foreach (RegistryKey thisKey in subKeys)
            {
                string thisDesc = thisKey.Name.Split('\\')[thisKey.Name.Split('\\').Length - 1];
                string thisUri = ""; //Not using a URI just yet.
                string thisMask = ""; //Also not using a Regex yet.
                ushort thisExpNew = 0; //0 is treated as "never expires" for both of these values.
                ushort thisExpMod = 0; //ushorts are cast to ints - this is free bounds checking.
                byte thisRecurseDepth = 0; //Optional
                bool thisDelEmpty = false; //Optional
                bool thisInsensitive = true; //Optional

                int dataLength = thisKey.GetValueNames().Length;
                string[,] values = new string[dataLength, 2];


                for (int i = 0; i < dataLength; i++) //populate values[,]
                {
                    values[i, 0] = thisKey.GetValueNames()[i];
                    values[i, 1] = thisKey.GetValue(values[i, 0]).ToString();
                }

                for (int i = 0; i < dataLength; i++) //Iterate through values[,]
                {
                    switch (values[i, 0].ToLowerInvariant())
                    {
                        case "uri":
                            if (Uri.IsWellFormedUriString(values[i, 1], UriKind.Absolute))
                                thisUri = values[i, 1];
                            break;
                        case "mask":
                            //By using Regex and discarding the path before we match,
                            //there is no possibility of illegal traversal.
                            thisMask = values[i, 1]; //Everything is escaped later, so no sanity checking here.
                            break;
                        case "expnew":
                            if (!ushort.TryParse(values[i, 1], out thisExpNew))
                                thisExpNew = 0; //TryParse fails destructively
                            break;
                        case "expmod":
                            if (!ushort.TryParse(values[i, 1], out thisExpMod))
                                thisExpMod = 0;
                            break;
                        case "recursedepth":
                            if (!byte.TryParse(values[i, 1], out thisRecurseDepth))
                                thisRecurseDepth = 0;
                            break;
                        case "delempty":
                            if (!bool.TryParse(values[i, 1], out thisDelEmpty))
                                thisDelEmpty = false;
                            break;
                        case "caseinsensitive":
                            if (!bool.TryParse(values[i, 1], out thisInsensitive))
                                thisInsensitive = true;
                            break;
                    }
                }

                //Do we have a valid job?
                if ((thisExpNew.Equals(0) && thisExpMod.Equals(0)) || thisUri.Equals("") || thisMask.Equals(""))
                    continue; //Bail on this job.

                //Escape everything, but immediately unescape the two wildcards. Also prepend a caret.
                thisMask = '^' + Regex.Escape(thisMask).Replace("\\*", "*").Replace("\\?", "?");
                Regex thisRegex;

                if (thisInsensitive)
                    thisRegex = new Regex(thisMask, RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
                else
                    thisRegex = new Regex(thisMask, RegexOptions.Compiled | RegexOptions.CultureInvariant);

                ploxDescs.Add(thisDesc);
                ploxRegexes.Add(thisRegex);
                ploxUris.Add(new Uri(thisUri)); //Already validated
                ploxExpNew.Add(thisExpNew);
                ploxExpMod.Add(thisExpMod);
                ploxRecurseDepth.Add(thisRecurseDepth);
                ploxDelEmpty.Add(thisDelEmpty);
            } //End of foreach - all jobs parsed

            return true;
        } //End of GetJobs

        private void LogEvent(String message, EventLogEntryType severity)
        {
            LogEvent(message, severity, true);
        }

        private void LogEvent(String message, EventLogEntryType severity, bool permitLogToFile)
        {
            if (this.logToEventLog) //Log the error to the Windows event log
            {
                try
                {
                    EventLog.WriteEntry(message, severity); //Simple!
                }
                catch { } //If this fails, I can't excatly log it, so the error gets lost.
            }

            if (this.logToFlatFile && permitLogToFile) //Log the error to a flat file
            {
                string errorMsg = DateTime.Now.ToString() + " : ";

                if (severity.Equals(EventLogEntryType.Information))
                    errorMsg += "Information : ";
                else if (severity.Equals(EventLogEntryType.Warning))
                    errorMsg += "Warning : ";
                else if (severity.Equals(EventLogEntryType.Error))
                    errorMsg += "Error : ";
                else if (severity.Equals(EventLogEntryType.SuccessAudit))
                    errorMsg += "Success : ";
                else if (severity.Equals(EventLogEntryType.FailureAudit))
                    errorMsg += "Failure : ";

                message = message.Replace('\n', ' '); //Replace newlines with spaces so entries are always one line.
                errorMsg += message; //Append the error description
                errorMsg += '\n'; //And a newline

                try
                {
                    File.AppendAllText(this.logFile, errorMsg); //Write the event
                }
                catch (Exception x) //Couldn't write to the log file
                {
                    //Infinite recursion avoidance - an error in an error logging routine would need to be logged, naturally.
                    LogEvent("Plox was unable to write an event to the log file at " +
                    this.logFile + ". The error returned was:" + '\n' + x.Message +
                    "\n\n" + "The log entry that could not be written was:" + '\n' + errorMsg,
                    EventLogEntryType.Error, false);
                }
            }
        }

        private List<Uri> Explode(Uri jobUri, byte recurseDepth, bool retEmptyDirsOnly)
        {
            string jobPath = Uri.UnescapeDataString(jobUri.PathAndQuery);

            switch (jobUri.Scheme.ToLowerInvariant()) //Scheme handlers - they may take a very long time to finish!
            {
                case "file":
                    return(Explode(jobPath, recurseDepth, false));

                case "ftp":
                    return(Explode(MakeFTP(jobUri), jobPath, recurseDepth, false));
            }

            return(new List<Uri>()); //In the event of an unhandled scheme
        }

        private List<Uri> Explode(string absPath, byte recurseDepth, bool retEmptyDirsOnly) //For local filesystems
        {
            //Date is an OLE Automation Date stored as a string as a URI fragment - 
            //it must be trimmed, parsed to a double, and then to a DateTime.
            //ex: DateTime.FromOADate(double.Parse(myUri.Fragment.Trim(new char[] { '#' })));
            List<Uri> myData = new List<Uri>();

            //Windows paths use backslashes.
            absPath = absPath.Replace('/', '\\');

            if (!absPath.StartsWith("\\")) //Bad path
                return myData;

            if (!absPath.EndsWith("\\")) //Tag a slash on for consistency
                absPath += '\\';

            //Structured differently from FTP Explode - since we must explicitly request either directories or
            //files when listing contents, we generate file URIs first and then we explode all subdirectories.
            try
            {
                if (!retEmptyDirsOnly)
                {
                    foreach (FileInfo thisFile in new DirectoryInfo(absPath).GetFiles())
                    {
                        //Don't need to replace the directory separators with '/', but I think it's good
                        //coding practice to assume that the Framework will not automatically fix this.

                        //Also - Microsoft tends to construct URIs that look like "file:///c:/foo". This works
                        //in Windows, but is only kinda-sorta in compliance with RFC 3986. Specifically, Windows
                        //uses ':' as the volume separator char, and this causes problems parsing the URI fragment.
                        //So, we escape the path. This way everybody can be happy.
                        myData.Add(new Uri("file:///" + Uri.EscapeDataString(
                            thisFile.FullName.Replace('\\', '/').TrimStart('/')) +
                            '#' + thisFile.LastWriteTimeUtc.ToOADate() + ':' + thisFile.CreationTimeUtc.ToOADate()));
                    }
                }

                foreach (DirectoryInfo thisDir in new DirectoryInfo(absPath).GetDirectories())
                {
                    if (retEmptyDirsOnly && thisDir.GetFileSystemInfos().Length == 0)
                        myData.Add(new Uri("file:///" + Uri.EscapeDataString(
                            thisDir.FullName.Replace('\\', '/').TrimStart('/')) + '/'));

                    if (recurseDepth > 0)
                        myData.AddRange(Explode(absPath + thisDir.ToString() + '/', (byte)(recurseDepth - 1), retEmptyDirsOnly));
                }
            }

            catch
            {
                if (this.logOnLocalEnumFail)
                    LogEvent("A filesystem error occurred during enumeration. The failing path is: " +
                        '\n' + absPath, EventLogEntryType.Information);
            }

            return myData;
        }

        private List<Uri> Explode(FTP site, string absPath, byte recurseDepth, bool retEmptyDirsOnly) //For FTP sites
        {
            //Date is an OLE Automation Date stored as a string as a URI fragment - 
            //it must be trimmed, parsed to a double, and then to a DateTime.
            //ex: DateTime.FromOADate(double.Parse(myUri.Fragment.Trim(new char[] { '#' })));
            List<Uri> myData = new List<Uri>();

            if (!absPath.StartsWith("/")) //Bad path
                return myData;

            if (!absPath.EndsWith("/")) //Tag a slash on for consistency
                absPath += '/';

            try
            {
                if (!site.Connected())
                    try
                    {
                        site.Connect();
                    }
                    catch (Exception x)
                    {
                        LogEvent("A FTP error occurred. The message returned was: " + x.Message, EventLogEntryType.Warning);
                        return myData; //We are done.
                    }

                site.ChangeDir(absPath);
                site.List();

                foreach (FTPListItem thisItem in site.DirectoryListing) //Cached, so I can reuse site in the loops.
                {
                    if (!retEmptyDirsOnly)
                    {
                        if (thisItem.ItemType.Equals(DirItemType.ditDirectory) && recurseDepth > 0)
                            myData.AddRange(Explode(site, absPath + thisItem.FileName + '/', (byte)(recurseDepth - 1), retEmptyDirsOnly)); //Recursion
                        else if (thisItem.ItemType.Equals(DirItemType.ditFile) && thisItem.ModifiedAvail)
                            myData.Add(new Uri("ftp://" + Uri.EscapeDataString(site.Username) + ':' + Uri.EscapeDataString(site.Password) +
                                '@' + site.Host + ':' + site.Port + '/' + absPath.TrimStart('/') + thisItem.FileName +
                                '#' + thisItem.ModifiedDate.ToString() + ':'));
                    }
                    else
                    {
                        if (thisItem.ItemType.Equals(DirItemType.ditDirectory))
                        {
                            //Since we don't touch absPath, we don't need to change this back.
                            site.ChangeDir(absPath + thisItem.FileName + '/');
                            site.List();

                            if (site.DirectoryListing.Count == 0)
                            {
                                myData.Add(new Uri("ftp://" + Uri.EscapeDataString(site.Username) + ':' + Uri.EscapeDataString(site.Password) +
                                    '@' + site.Host + ':' + site.Port + absPath + thisItem.FileName + '/'));
                            }
                            if (recurseDepth > 0) //Actually, we look n+1 deep, since we open directories to check emptiness.
                                myData.AddRange(Explode(site, absPath + thisItem.FileName + '/', (byte)(recurseDepth - 1), retEmptyDirsOnly)); //Recursion
                        }
                    }
                }
            }
            catch
            {
                if (this.logOnFTPError && !this.redactFTPCredentials)
                    LogEvent("A FTP error occurred during enumeration. The failing path is: " +
                        '\n' + "ftp://" + Uri.UnescapeDataString(site.Username) + ':' + Uri.UnescapeDataString(site.Password) +
                        '@' + site.Host + ':' + site.Port + absPath, EventLogEntryType.Information);
                else if (this.logOnFTPError && this.redactFTPCredentials)
                    LogEvent("A FTP error occurred during enumeration. The failing path is: " +
                        '\n' + "ftp://" + site.Host + ':' + site.Port + absPath, EventLogEntryType.Information);
            }

            return myData;
        }

        private List<Uri> DatePrune(List<Uri> input, ushort expMod, ushort expNew)
        {
            //Prune returns the list of files to be deleted. If the incoming Uri list contains no
            //date information (a directory list), Prune will return an empty list.

            List<Uri> output = new List<Uri>();

            //Everything is done in UTC time for obvious reasons.
            double jobStart = DateTime.UtcNow.ToOADate();
            double modCutoff = DateTime.MaxValue.ToOADate();
            double newCutoff = DateTime.MaxValue.ToOADate();

            if (expMod != 0)
                modCutoff = jobStart - expMod;

            if (expNew != 0)
                newCutoff = jobStart - expNew;

            foreach (Uri thisUri in input)
            {
                //Break the fragment into either 0, 1, or 2 parts
                string[] thisFrag = thisUri.Fragment.TrimStart('#').
                    Split(new char[] { ':' }, 2, StringSplitOptions.RemoveEmptyEntries);

                //Parse the strings into OADates - the first is modification and the second is creation.
                Queue<double> myDates = new Queue<double>();
                foreach (string thisDate in thisFrag)
                    myDates.Enqueue(double.Parse(thisDate));

                //Also, we strip the fragments - they add parse complexity and are no longer needed.
                //Truncating in this way ensures that it is done properly - tokenizing the string
                //could lead to unexpected results if another # is present.
                Uri truncUri = new Uri(thisUri.OriginalString.Substring(0,
                    thisUri.OriginalString.Length - thisUri.Fragment.Length));

                if (myDates.Count != 0) //If mod date is present
                    if (myDates.Dequeue() - modCutoff < 0) //Too old
                    {
                        output.Add(truncUri);
                        continue; //No need to add it again.
                    }

                if (myDates.Count != 0) //If creation date is present
                    if (myDates.Dequeue() - newCutoff < 0) //Too old
                    {
                        output.Add(truncUri);
                        continue;
                    }
            }

            return output;
        }

        private List<Uri> MaskPrune(List<Uri> input, Regex mask)
        {
            //We assume that the fragment has already been stripped, and that the Uri is still escaped.
            //Since the Uri list has been run through DatePrune and checked for well-formedness already,
            //it is not necessary to sanity check for the fragment. And the mask has already been checked
            //for well-formedness and presence by GetJobs, so no need to check that either.

            List<Uri> output = new List<Uri>();

            foreach (Uri thisUri in input)
            {
                //And the nice thing is, we know by now that the only directory separator is '/'
                string absPath = Uri.UnescapeDataString(thisUri.PathAndQuery);
                //By discarding the path before evaluating the Regex, we eliminate directory traversal.
                string thisFile = absPath.Split(new char[] { '/' })[absPath.Split(new char[] { '/' }).Length - 1]; //Zero-indexed!

                if (mask.IsMatch(thisFile))
                    output.Add(thisUri);
            }

            return output;
        }

        private void PloxDo(object source, ElapsedEventArgs args) //Fired by ploxTimer
        {
            for (int i = 0; i < this.ploxUris.Count; i++) //Indexing multiple arrays
            {
                //I'd like to coalesce jobs sharing an authority, but if ExecJob is multithreaded, it becomes
                //difficult to do so. I don't think it will become necessary since the performance gains would
                //probably be minimal, but I thought I'd comment it here to remind myself to take another look
                //at this in the future. There does, however need to be an option to not multithread jobs, in
                //the case of a FTP server that only allows 1 or 2 connections per IP with multiple jobs.

                //Also, there needs to be a way for Plox to determine if it is still running when ploxTimer
                //fires - right now, if the timer is too short, Plox can threadflood.

                //This way, if a job fails, all it has to do is return. Logging is handled by ExecJob.
                ExecJob(this.ploxDescs[i], this.ploxUris[i], this.ploxRegexes[i], this.ploxExpNew[i],
                    this.ploxExpMod[i], this.ploxRecurseDepth[i], this.ploxDelEmpty[i]);
            }
        }

        private void ExecJob(string jobDesc, Uri jobUri, Regex jobRegex, ushort expNew, ushort expMod, byte recurseDepth, bool delEmpty)
        {
            //So, jobPath will be a URI, and the following are all legal:
            //
            //file:///c:/foo
            //file:///c:/foo/
            //file:///usr/bin            
            //file:///usr/bin/
            //ftp://ftp.foo.net/baz
            //ftp://ftp.foo.net/baz/
            //
            //Yes, local paths generated by PloxEd 1.0 (c:\foo) are no longer supported. The installed base for
            //Plox so far is 1, and it's in the other room, so I feel a breaking change is appropriate here. All
            //future versions of PloxEd WILL store values as a URI, which is more multiplatform-correct anyway -
            //it's a nice, platform-neutral way of doing things. Also, URIs will be unescaped before processing.
            //Shouts go to Macromedia, which uses "#" in pathnames, which exposed a bug in Explode, and shouts go
            //to Microsoft, who did not correctly implement Uri.EscapeUriString (Which does not escape # or unwise)
            //and Uri.EscapeDataString (Which escapes #, but not unwise). Even though I use Uri.EscapeDataString.
            //It's not actually incorrect to leave unwise unescaped, but it's sloppy to be sure. I'll move away
            //from Microsoft's Uri class in the future. Also, the SMB and CIFS schemes are not yet implemented,
            //There are obvious external workarounds for this. Post 2.0 will support this feature...

            //Every job must be Exploded, Pruned, and then Deleted, in that order. But the individual jobs are not
            //interdependent, so it's very possible to multithread this method. The performance gains for local
            //batches would be minimal (may even slow down slightly due to overlapping seeks), but the performance
            //gains for FTP batches would be enormous. This should be added post 2.0.

            string jobScheme = jobUri.Scheme.ToLowerInvariant();

            if (!this.supportedSchemes.Contains(jobScheme))
                return; //Bail, unsupported scheme

            //If a scheme is unhandled by explode, it will return an empty list.
            List<Uri> fileList = Explode(jobUri, recurseDepth, false);

            if (fileList.Count.Equals(0)) //Nothing to do.
                return;

            //Now, we must process the mask into a Regex. Basically, I am trying to avoid exposing the user to
            //regular expressions - Plox is targeted at power users, not developers. There may be an option
            //to disable this behavior in a future version.
            
            fileList = MaskPrune(DatePrune(fileList, expMod, expNew), jobRegex); //Also strips the Uri fragment.

            string[] pathArray = PathStrip(fileList); //Don't want the authority and scheme anymore.

            Delete(jobDesc, jobUri, pathArray);

            if (!delEmpty)
                return; //Bail, done with this job.

            string[] dirList = PathStrip(Explode(jobUri, recurseDepth, true));

            if (dirList.Length.Equals(0)) //Nothing to do.
                return;

            RmDir(jobDesc, jobUri, dirList);
        } //End of ExecJob

        private void RmDir(string jobDesc, Uri jobUri, string[] files)
        {
            //All schemes are simply passed to the appropriate overload

            switch (jobUri.Scheme.ToLowerInvariant())
            {
                case "file":
                    RmDir(jobDesc, files);
                    return;

                case "ftp":
                    RmDir(jobDesc, MakeFTP(jobUri), files);
                    return;

                case "cifs": //Falls through
                case "smb": //Not implemented
                    return;
            }
        }

        private void RmDir(string jobDesc, string[] dirs)
        {
            foreach (string thisDir in dirs)
            {
                try
                {
                    Directory.Delete(thisDir); //Only works if the directory really is empty.
                }
                catch (Exception x)
                {
                    LogEvent("Plox could not delete a directory. Please ensure that Plox " +
                        "is running as LocalSystem or as another account with sufficient credentials. " +
                        "This error can also occur if the target file is currently in use." +
                        "\n\n" + "Job Name: " + jobDesc + '\n' + "File: " + thisDir +
                        "\n\n" + x.Message, EventLogEntryType.Information);
                }
            }
        }

        private void RmDir(string jobDesc, FTP site, string[] dirs)
        {
            if (!site.Connected())
                try
                {
                    site.Connect();
                }
                catch (Exception x)
                {
                    LogEvent("A FTP error occurred. The message returned was: " + x.Message, EventLogEntryType.Warning);
                    return; //We are done.
                }

            foreach (string thisDir in dirs)
            {
                try
                {
                    site.RemoveDir(thisDir);
                }
                catch (Exception x)
                {
                    LogEvent("Plox could not delete a remote directory. Please ensure that Plox " +
                        "was provided an account with sufficient credentials." +
                        "\n\n" + "Job Name: " + jobDesc + '\n' + "File: " + thisDir +
                        "\n\n" + x.Message, EventLogEntryType.Information);
                }
            }
        }

        private void Delete(string jobDesc, string[] files)
        {
            foreach (string thisFile in files)
            {
                try
                {
                    File.Delete(thisFile);
                }
                catch (Exception x)
                {
                    LogEvent("Plox could not delete a file. Please ensure that Plox " +
                        "is running as LocalSystem or as another account with sufficient credentials. " +
                        "This error can also occur if the target file is currently in use." +
                        "\n\n" + "Job Name: " + jobDesc + '\n' + "File: " + Uri.UnescapeDataString(thisFile) +
                        "\n\n" + x.Message, EventLogEntryType.Information);
                }
            }
        }

        private void Delete(string jobDesc, FTP site, string[] files)
        {
            if (!site.Connected())
                try
                {
                    site.Connect();
                }
                catch (Exception x)
                {
                    LogEvent("A FTP error occurred. The message returned was: " + x.Message, EventLogEntryType.Warning);
                    return; //We are done.
                }

            foreach (string thisFile in files)
            {
                try
                {
                    site.Delete(thisFile);
                }
                catch (Exception x)
                {
                    LogEvent("Plox could not delete a remote file. Please ensure that Plox " +
                        "was provided an account with sufficient credentials." +
                        "\n\n" + "Job Name: " + jobDesc + '\n' + "File: " + Uri.UnescapeDataString(thisFile) +
                        "\n\n" + x.Message, EventLogEntryType.Information);
                }
            }
        }

        private void Delete(string jobDesc, Uri jobUri, string[] files)
        {
            //All schemes are simply passed to the appropriate overload

            switch (jobUri.Scheme.ToLowerInvariant())
            {
                case "file":
                    Delete(jobDesc, files);
                    return;

                case "ftp":
                    Delete(jobDesc, MakeFTP(jobUri), files);
                    return;

                case "cifs": //Falls through
                case "smb": //Not implemented
                    return;
            }
        }

        private string[] PathStrip(List<Uri> uriList)
        {
            Uri[] uriArray = new Uri[uriList.Count];
            uriList.CopyTo(uriArray);

            string[] output = new string[uriArray.Length];

            for (int i = 0; i < uriArray.Length; i++)
                output[i] = uriArray[i].AbsolutePath;

            return output;
        }

        private FTP MakeFTP(Uri jobUri)
        {
            FTP jobFTP = new FTP();
            jobFTP.Host = jobUri.Host;
            jobFTP.Port = jobUri.Port;

            if (!jobUri.UserInfo.Contains(":")) //Split will return nulls...
            {
                jobFTP.Username = "anonymous"; //There's a 68.71% chance this will work.
                jobFTP.Password = "jbloggs@b1t.net"; //Joe Bloggs rides again!
            }
            else //Use the user-supplied data
            {
                try
                {
                    jobFTP.Username = Uri.UnescapeDataString(jobUri.UserInfo.Split(new char[] { ':' }, 2, StringSplitOptions.None)[0]);
                    jobFTP.Password = Uri.UnescapeDataString(jobUri.UserInfo.Split(new char[] { ':' }, 2, StringSplitOptions.None)[1]);
                }
                catch //I can't think of anything that would trigger an exception, but...
                {
                    jobFTP.Username = "anonymous";
                    jobFTP.Password = "jbloggs@b1t.net";
                }
            }

            return jobFTP;
        }

    } //End of class (PloxSvc)
} //End of namespace (Plox)
